Simple Tree Model mit User-Object
Bäume sind immer interessant, finde ich. Sie sind nicht so eintönig gleichmäßig wie die meisten Listen. Der CL_SIMPLE_TREE_MODEL hat jedoch zudem auch in der Programmierung eine schöne Besonderheit: Zu jedem Knoten kann die Instanz einer beliebigen Klasse übergeben werden. Mit der Methode NODE_GET_USER_OBJECT kann man sich dann die Instanz geben lassen und hiermit weiter arbeiten.
Tree-Ausgabe
Als Beispielaufgabe habe ich mir die folgende gestellt: Zeige alle Materialien zu einer Selektion nach Materialart unterteilt an. Also: Hauptknoten – Materialart – Materialien. Die Selektion der Daten erfolgt im Selektionsbild des Reports bei AT SELECTION-SCREEN. Auf unserem IDES-System haben ich nur ein paar Dutzend Materialien, da geht die Selektion zügig.
Mit Doppelklick auf einen Eintrag soll – je nach Knotenart “Materialart” oder “Materialnummer” – die jeweilige Information ausgegeben werden. Es muss also bei dem Knoten “Materialart” eine andere Klasse verwendet werden, als bei der Knotenart “Materialnummer”.
Zusätzlich zur Demonstration des CL_SIMPLE_TREE_MODEL habe ich versucht, möglichst viele neue Sprachbefehle zu verwenden. Gerade bei der Verwendung des User-Objects macht der Befehl NEW zur Instantiierung einer Klasse die Programmierung wirklich elegant.
User-Object
für das User-Object habe ich eine Hauptklasse LCL_USER_OBJECT_MARA definiert:
CLASS lcl_user_object_mara DEFINITION ABSTRACT. PUBLIC SECTION. DATA mara TYPE mara. METHODS constructor IMPORTING i_mara TYPE mara. METHODS get_text. ENDCLASS. CLASS lcl_user_object_mara IMPLEMENTATION. METHOD constructor. mara = i_mara. ENDMETHOD. METHOD get_text. SELECT SINGLE maktx FROM makt INTO @DATA(text) WHERE matnr = @mara-matnr AND spras = @sy-langu. IF sy-subrc = 0. MESSAGE text TYPE 'I'. ENDIF. ENDMETHOD. ENDCLASS.
Die Klasse hat nur das Attribut MARA, das bei der Erzeugung mitgegeben werden muss und die Methode GET_TEXT mit der ein Text zum Objekt ermittelt wird.
Da ich die Klasse als ABSTRAKT definiert habe, kann ich diese Klasse nicht instantiieren. Das geht nur bei den von dieser Klasse abgeleiteten Klassen:
CLASS lcl_user_object_mtart DEFINITION INHERITING FROM lcl_user_object_mara. PUBLIC SECTION. METHODS get_text REDEFINITION. ENDCLASS. CLASS lcl_user_object_mtart IMPLEMENTATION. METHOD get_text. MESSAGE |Materialart { mara-mtart }| TYPE 'I'. ENDMETHOD. ENDCLASS. CLASS lcl_user_object_matnr DEFINITION INHERITING FROM lcl_user_object_mara. PUBLIC SECTION. ENDCLASS.
In der Klasse MTART redefiniere ich die Methode “GET_TEXT” um einen eigenen Text für Materialart zu bekommen.
Die Klasse MATNR ist nur eine leere Hülle, da sie alles andere von der Hauptklasse erbt.
Natürlich hätte ich auch zwei komplett unterschiedliche und voneinander unabhängige Klassen definieren können.
Hauptprogramm
Das Hauptprogramm besteht nur den Selektionsparametern und aus zwei Ereignissen:
- dem Ereignis INITIALIZATION, in dem ich den Docking-Container erzeuge und
- dem Ereignis AT SELECTION-SCREEN, in dem ich die Daten selektiere und den Baum erzeuge
REPORT z_simple_tree_model. DATA s_mara TYPE mara. SELECT-OPTIONS s_matnr FOR s_mara-matnr. SELECT-OPTIONS s_mtart FOR s_mara-mtart. [...Klassendefinition...] INITIALIZATION. lcl_main=>create_docker( ). AT SELECTION-SCREEN. TRY. lcl_main=>get_data( ). lcl_main=>create_tree( ). lcl_main=>add_nodes( ). CATCH lcx_error. MESSAGE 'Fehler bei Selektion' TYPE 'I'. ENDTRY.
Hauptklasse
Die Klasse LCL_MAIN, in der die Logik des Programms vorhanden ist, besteht aus diesen vier Methoden:
- Create_Docker
- Create_Tree
- Get_Data
- Add_Nodes
Zusätzlich gibt es noch die Methode zur Ereignisbehandlung des Doppelklicks auf einen Knoten: Handle_Node_Double_Click.
Create_Docker
So simple:
IF docker IS INITIAL. docker = NEW #( side = cl_gui_docking_container=>dock_at_bottom ratio = 50 ). ENDIF.
Get_Data
Ebenfalls nicht spektakulär:
SELECT * FROM mara INTO TABLE t_mara WHERE matnr IN s_matnr AND mtart IN s_mtart. IF sy-subrc > 0. RAISE EXCEPTION TYPE lcx_error. ENDIF.
Zur Fehlerbehandlung habe ich eine eigene Exception-class erstellt:
CLASS lcx_error DEFINITION INHERITING FROM cx_no_check. ENDCLASS.
Create_Tree
Bei der Erzeugung des CL_SIMPLE_TREE_MODEL bin ich auf die erste Hürde gestoßen, denn die Erzeugung des Control läuft etwas anders, als bei den meisten anderen GUI-Controls. Normalerweise instantiiert man das GUI-Control unter Angabe des Containers in das das Control eingefügt werden soll (Parameter PARENT). Nicht so bei dieser Klasse. Hier wird erst das Tree-Objekt erzeugt und danach mit der Methode CREATE_TREE_CONTROL an den PARENT-Container gehängt:
IF tree IS BOUND. tree->delete_all_nodes( ). ELSE. tree = NEW #( node_selection_mode = cl_simple_tree_model=>node_sel_mode_single ). tree->create_tree_control( EXPORTING parent = docker ). tree->set_registered_events( EXPORTING events = VALUE #( ( eventid = cl_simple_tree_model=>eventid_node_double_click appl_event = space ) ) ). SET HANDLER handle_node_double_click FOR tree. ENDIF. tree->add_node( node_key = 'Root' "#EC NOTEXT isfolder = 'X' text = 'Materialien zur Selektion' ).
Das Event Doppelklick wird hier ebenfalls registriert und der event handler dafür installiert. Zusätzlich wird der Hauptknoten ROOT eingefügt.
Add_Nodes
Mit der Methode ADD_NODE des Tree-Controls werden einzelne Knoten in den Baum eingehängt. Immer unter Angabe des Knoten-ID, des übergeordneten Knotens, Text und ein paar anderen. An dieser Stelle kann das User-Object übergeben werden, dass dann zur Knoten-ID zur Verfügung steht:
DATA l_mtart TYPE mtart. SORT t_mara BY mtart matnr. LOOP AT t_mara INTO DATA(mara). IF l_mtart <> mara-mtart. l_mtart = mara-mtart. "Knoten MATERIALART tree->add_node( node_key = CONV #( mara-mtart ) relative_node_key = 'Root' relationship = cl_simple_tree_model=>relat_last_child isfolder = 'X' text = |Materialart { mara-mtart } | user_object = NEW lcl_user_object_mtart( i_mara = mara ) ). ENDIF. "Knoten MATERIALNUMMER tree->add_node( node_key = |{ mara-matnr ALPHA = OUT }| relative_node_key = CONV #( mara-mtart ) relationship = cl_simple_tree_model=>relat_last_child isfolder = space text = |{ mara-matnr ALPHA = OUT }| user_object = NEW lcl_user_object_matnr( i_mara = mara ) ). ENDLOOP. tree->expand_root_nodes( ).
Ereignisbehandler
Im Ereignisbehandler prüfen wir, welchen Objekttyp das User-Object hat, um gegebenenfalls anders reagieren zu können:
DATA o_user_object_matnr TYPE REF TO lcl_user_object_matnr. DATA o_user_object_mtart TYPE REF TO lcl_user_object_mtart. DATA o_object TYPE REF TO object. tree->node_get_user_object( EXPORTING node_key = node_key IMPORTING user_object = o_object ). IF o_object IS INSTANCE OF lcl_user_object_mtart. o_user_object_mtart ?= o_object. o_user_object_mtart->get_text( ). EXIT. ENDIF. IF o_object IS INSTANCE OF lcl_user_object_matnr. o_user_object_matnr ?= o_object. o_user_object_matnr->get_text( ). EXIT. ENDIF.
In diesem Fall verwenden wir zwar für MTART und MATNR die gleiche Methode GET_TEXT, aber hier könnte man je Objekt eine andere Funktion ausführen. Falls im ABAP Release die Syntax IS INSTANCE OF noch nicht verfügbar ist, muss mit TRY – CATCH geprüft werden, ob der Cast zwischen OBJECT und User-Object erfolgreich war oder nicht:
TRY. o_user_object_matnr ?= o_object. o_user_object_matnr->get_text( ). CATCH cx_sy_move_cast_error. ENDTRY.
Das komplette Programm
REPORT zdemo_simple_tree_model. DATA s_mara TYPE mara. SELECT-OPTIONS s_matnr FOR s_mara-matnr. SELECT-OPTIONS s_mtart FOR s_mara-mtart. CLASS lcx_error DEFINITION INHERITING FROM cx_no_check. ENDCLASS. CLASS lcl_user_object_mara DEFINITION ABSTRACT. PUBLIC SECTION. DATA mara TYPE mara. METHODS constructor IMPORTING i_mara TYPE mara. METHODS get_text. ENDCLASS. CLASS lcl_user_object_mara IMPLEMENTATION. METHOD constructor. mara = i_mara. ENDMETHOD. METHOD get_text. SELECT SINGLE maktx FROM makt INTO @DATA(text) WHERE matnr = @mara-matnr AND spras = @sy-langu. IF sy-subrc = 0. MESSAGE text TYPE 'I'. ENDIF. ENDMETHOD. ENDCLASS. CLASS lcl_user_object_mtart DEFINITION INHERITING FROM lcl_user_object_mara. PUBLIC SECTION. METHODS get_text REDEFINITION. ENDCLASS. CLASS lcl_user_object_mtart IMPLEMENTATION. METHOD get_text. MESSAGE |Materialart { mara-mtart }| TYPE 'I'. ENDMETHOD. ENDCLASS. CLASS lcl_user_object_matnr DEFINITION INHERITING FROM lcl_user_object_mara. PUBLIC SECTION. ENDCLASS. CLASS lcl_main DEFINITION. PUBLIC SECTION. CLASS-METHODS get_data. CLASS-METHODS create_tree. CLASS-METHODS add_nodes. CLASS-METHODS create_docker. CLASS-METHODS handle_node_double_click FOR EVENT node_double_click OF cl_simple_tree_model IMPORTING node_key. CLASS-DATA docker TYPE REF TO cl_gui_docking_container. CLASS-DATA tree TYPE REF TO cl_simple_tree_model. CLASS-DATA t_mara TYPE STANDARD TABLE OF mara. CLASS-DATA s_mara TYPE mara. ENDCLASS. CLASS lcl_main IMPLEMENTATION. METHOD create_docker. IF docker IS INITIAL. docker = NEW #( side = cl_gui_docking_container=>dock_at_bottom ratio = 50 ). ENDIF. ENDMETHOD. METHOD handle_node_double_click. DATA o_user_object_matnr TYPE REF TO lcl_user_object_matnr. DATA o_user_object_mtart TYPE REF TO lcl_user_object_mtart. DATA o_object TYPE REF TO object. tree->node_get_user_object( EXPORTING node_key = node_key IMPORTING user_object = o_object ). IF o_object IS INSTANCE OF lcl_user_object_mtart. o_user_object_mtart ?= o_object. o_user_object_mtart->get_text( ). EXIT. ENDIF. IF o_object IS INSTANCE OF lcl_user_object_matnr. o_user_object_matnr ?= o_object. o_user_object_matnr->get_text( ). EXIT. ENDIF. ENDMETHOD. METHOD get_data. SELECT * FROM mara INTO TABLE t_mara WHERE matnr IN s_matnr AND mtart IN s_mtart. IF sy-subrc > 0. RAISE EXCEPTION TYPE lcx_error. ENDIF. ENDMETHOD. METHOD create_tree. IF tree IS BOUND. tree->delete_all_nodes( ). ELSE. tree = NEW #( node_selection_mode = cl_simple_tree_model=>node_sel_mode_single ). tree->create_tree_control( EXPORTING parent = docker ). tree->set_registered_events( EXPORTING events = VALUE #( ( eventid = cl_simple_tree_model=>eventid_node_double_click appl_event = space ) ) ). SET HANDLER handle_node_double_click FOR tree. ENDIF. tree->add_node( node_key = 'Root' "#EC NOTEXT isfolder = 'X' text = 'Materialien zur Selektion' ). ENDMETHOD. METHOD add_nodes. DATA l_mtart TYPE mtart. SORT t_mara BY mtart matnr. LOOP AT t_mara INTO DATA(mara). IF l_mtart <> mara-mtart. l_mtart = mara-mtart. "Knoten MATERIALART tree->add_node( node_key = CONV #( mara-mtart ) relative_node_key = 'Root' relationship = cl_simple_tree_model=>relat_last_child isfolder = 'X' text = |Materialart { mara-mtart } | user_object = NEW lcl_user_object_mtart( i_mara = mara ) ). ENDIF. "Knoten MATERIALNUMMER tree->add_node( node_key = |{ mara-matnr ALPHA = OUT }| relative_node_key = CONV #( mara-mtart ) relationship = cl_simple_tree_model=>relat_last_child isfolder = space text = |{ mara-matnr ALPHA = OUT }| user_object = NEW lcl_user_object_matnr( i_mara = mara ) ). . ENDLOOP. tree->expand_root_nodes( ). ENDMETHOD. ENDCLASS. INITIALIZATION. lcl_main=>create_docker( ). AT SELECTION-SCREEN. TRY. lcl_main=>get_data( ). lcl_main=>create_tree( ). lcl_main=>add_nodes( ). CATCH lcx_error. MESSAGE 'Fehler bei Selektion' TYPE 'I'. ENDTRY.
- Interview mit Björn Schulz (Software-Heroes.com) - 3. September 2024
- Daten aus ALV ermitteln - 3. September 2024
- So lange es den SAPGUI noch gibt… - 27. Juni 2024